#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <malloc.h>

#include <psp2/io/devctl.h>
#include <psp2/io/dirent.h>
#include <psp2/io/fcntl.h>
#include <psp2/io/stat.h>
#include <png.h>

#include "emu/emu.h"
#include "file.h"
#include "utils.h"

#define SCE_ERROR_ERRNO_EEXIST 0x80010011

int HasEndSlash(const char *path)
{
    return path[strlen(path) - 1] == '/';
}

int RemoveEndSlash(char *path)
{
    int len = strlen(path);

    if (path[len - 1] == '/')
    {
        path[len - 1] = '\0';
        return 1;
    }

    return 0;
}

int AddEndSlash(char *path)
{
    int len = strlen(path);
    if (len < MAX_PATH_LENGTH - 2)
    {
        if (path[len - 1] != '/')
        {
            path[len] = '/';
            path[len + 1] = '\0';
            return 1;
        }
    }

    return 0;
}

int MakeParentDirEx(char *parent, int parent_size, const char *path, int path_len)
{
    if (!parent || parent_size <= 0)
        return -1;
    parent[0] = '\0';

    if (!path || path_len <= 0)
        return -1;

    if ((path_len == 1 && path[0] == '/') || path[path_len - 1] == ':')
    {
        if (parent_size > path_len)
        {
            strncpy(parent, path, path_len);
            parent[path_len] = '\0';
        }
        return -1;
    }

    if (path[path_len - 1] == '/')
        path_len--;

    int sep = -1;

    int i;
    for (i = path_len - 1; i >= 0; i--)
    {
        if (path[i] == '/' || path[i] == ':')
        {
            sep = i + 1;
            break;
        }
    }
    if (sep < 0)
        return -1;

    int new_len = sep;
    if (new_len > parent_size - 1)
        new_len = parent_size - 1;
    if (new_len <= 0)
        return -1;
    strncpy(parent, path, new_len);
    parent[new_len] = '\0';

    return 0;
}

int MakeFileNameEx(char *name, int name_size, const char *path, int path_len)
{
    if (!name || name_size <= 0)
        return -1;
    name[0] = '\0';

    if (!path || path_len <= 0)
        return -1;

    if (path[path_len - 1] == '/' || path[path_len - 1] == ':')
        return -1;

    int sep = -1;

    int i;
    for (i = path_len - 1; i >= 0; i--)
    {
        if (path[i] == '/' || path[i] == ':')
        {
            sep = i + 1;
            break;
        }
    }
    if (sep < 0)
        return -1;

    int new_len = path_len - sep;
    if (new_len > name_size - 1)
        new_len = name_size - 1;
    if (new_len <= 0)
        return -1;
    strncpy(name, path + sep, new_len);
    name[new_len] = '\0';

    return 0;
}

int MakeBaseNameEx(char *name, int name_size, const char *path, int path_len)
{
    if (!name || name_size <= 0)
        return -1;
    name[0] = '\0';

    if (!path || path_len <= 0)
        return -1;

    int head = 0;
    int sep = path_len;

    int i;
    for (i = path_len - 1; i >= 0; i--)
    {
        if (path[i] == '/' || path[i] == ':')
        {
            head = i + 1;
            break;
        }
        else if (path[i] == '.' && sep == path_len)
        {
            sep = i;
        }
    }

    int new_len = sep - head;
    if (new_len > name_size - 1)
        new_len = name_size - 1;
    if (new_len <= 0)
        return -1;
    strncpy(name, path + head, new_len);
    name[new_len] = '\0';

    return 0;
}

int MakeParentDir(char *parent_dir, const char *path, int size)
{
    return MakeParentDirEx(parent_dir, size, path, strlen(path));
}

int MakeFileName(char *name, const char *path, int size)
{
    return MakeFileNameEx(name, size, path, strlen(path));
}

int MakeBaseName(char *name, const char *path, int size)
{
    return MakeBaseNameEx(name, size, path, strlen(path));
}

int64_t ReadFileDesc(int fd, const void *buffer, size_t size)
{
    if (fd < 0)
        return fd;

    char *buf = (char *)buffer;
    size_t remaining = size;
    int transfer = TRANSFER_SIZE;
    int read;

    while (remaining > 0)
    {
        if (remaining < TRANSFER_SIZE)
            transfer = remaining;
        else
            transfer = TRANSFER_SIZE;

        read = sceIoRead(fd, buf, transfer);
        if (read < 0)
            return read;
        if (read == 0)
            break;

        buf += read;
        remaining -= read;
    }

    return size - remaining;
}

int64_t WriteFileDesc(int fd, const void *buffer, size_t size)
{
    if (fd < 0)
        return fd;

    char *buf = (char *)buffer;
    size_t remaining = size;
    int transfer = TRANSFER_SIZE;
    int written;

    while (remaining > 0)
    {
        if (remaining < TRANSFER_SIZE)
            transfer = remaining;
        else
            transfer = TRANSFER_SIZE;

        written = sceIoWrite(fd, buf, transfer);
        if (written < 0)
            return written;
        if (written == 0)
            break;

        buf += written;
        remaining -= written;
    }

    return size - remaining;
}

int64_t AllocateReadFileDesc(int fd, void **buffer, size_t size)
{
    if (fd < 0 || !buffer)
        return fd;

    void *buf = malloc(size);
    if (!buf)
        return -1;

    int64_t res = ReadFileDesc(fd, buf, size);
    if (res < 0)
    {
        free(buf);
        buf = NULL;
    }

    *buffer = buf;
    return res;
}

int64_t ReadFile(const char *file, const void *buffer, size_t size)
{
    int fd = sceIoOpen(file, SCE_O_RDONLY, 0);
    if (fd < 0)
        return fd;

    int64_t res = ReadFileDesc(fd, buffer, size);
    sceIoClose(fd);

    return res;
}

int64_t WriteFile(const char *file, const void *buffer, size_t size)
{
    int fd = sceIoOpen(file, SCE_O_WRONLY | SCE_O_CREAT | SCE_O_TRUNC, 0777);
    if (fd < 0)
        return fd;

    int64_t res = WriteFileDesc(fd, buffer, size);
    sceIoClose(fd);

    return res;
}

int64_t AllocateReadFile(const char *file, void **buffer, size_t *buffersize)
{
    int fd = sceIoOpen(file, SCE_O_RDONLY, 0);
    if (fd < 0)
        return fd;

    int64_t size = sceIoLseek(fd, 0, SCE_SEEK_END);
    if (size <= 0)
    {
        sceIoClose(fd);
        return -1;
    }

    sceIoLseek(fd, 0, SCE_SEEK_SET);
    int64_t res = AllocateReadFileDesc(fd, buffer, size);
    if (res < 0)
        size = 0;
    sceIoClose(fd);

    if (buffersize)
        *buffersize = size;

    return res;
}

int GetFileSize(const char *file)
{
    int fd = sceIoOpen(file, SCE_O_RDONLY, 0);
    if (fd < 0)
        return fd;

    int fileSize = sceIoLseek(fd, 0, SCE_SEEK_END);

    sceIoClose(fd);
    return fileSize;
}

int CheckFileExist(const char *file)
{
    SceIoStat st;
    if (sceIoGetstat(file, &st) >= 0)
        return SCE_S_ISREG(st.st_mode);
    return 0;
}

int CheckFolderExist(const char *folder)
{
    SceIoStat st;
    if (sceIoGetstat(folder, &st) >= 0)
        return SCE_S_ISDIR(st.st_mode);
    return 0;
}

int CreateFolder(const char *folder)
{
    int ret = 0;

    if (CheckFolderExist(folder))
        return 0;

    int len = strlen(folder);
    if (len <= 0)
        return -1;

    char *buf = malloc(len + 2);
    if (!buf)
        return -1;
    strcpy(buf, folder);
    if (buf[len - 1] != '/')
    {
        buf[len] = '/';
        buf[++len] = '\0';
    }

    char ch;
    int i;
    for (i = 0; i < len; i++)
    {
        if (buf[i] == '/')
        {
            ch = buf[i];
            buf[i] = '\0';
            ret = sceIoMkdir(buf, 0777);
            buf[i] = ch;
        }
    }

    free(buf);
    if (ret == SCE_ERROR_ERRNO_EEXIST)
        ret = 0;
    return ret;
}

int RemovePath(const char *path)
{
    SceUID dfd = sceIoDopen(path);
    if (dfd >= 0)
    {
        int res = 0;

        do
        {
            SceIoDirent dir;
            memset(&dir, 0, sizeof(SceIoDirent));

            res = sceIoDread(dfd, &dir);
            if (res > 0)
            {
                char *new_path = malloc(strlen(path) + strlen(dir.d_name) + 2);
                snprintf(new_path, MAX_PATH_LENGTH, "%s%s%s", path, HasEndSlash(path) ? "" : "/", dir.d_name);

                if (SCE_S_ISDIR(dir.d_stat.st_mode))
                {
                    int ret = RemovePath(new_path);
                    if (ret <= 0)
                    {
                        free(new_path);
                        sceIoDclose(dfd);
                        return ret;
                    }
                }
                else
                {
                    int ret = sceIoRemove(new_path);
                    if (ret < 0)
                    {
                        free(new_path);
                        sceIoDclose(dfd);
                        return ret;
                    }
                }

                free(new_path);
            }
        } while (res > 0);

        sceIoDclose(dfd);

        int ret = sceIoRmdir(path);
        if (ret < 0)
            return ret;
    }
    else
    {
        int ret = sceIoRemove(path);
        if (ret < 0)
            return ret;
    }

    return 1;
}

int CopyFile(const char *src_path, const char *dst_path)
{
    // The source and destination paths are identical
    if (strcasecmp(src_path, dst_path) == 0)
    {
        return -1;
    }

    // The destination is a subfolder of the source folder
    int len = strlen(src_path);
    if (strncasecmp(src_path, dst_path, len) == 0 && (dst_path[len] == '/' || dst_path[len - 1] == '/'))
    {
        return -1;
    }

    SceUID fdsrc = sceIoOpen(src_path, SCE_O_RDONLY, 0);
    if (fdsrc < 0)
        return fdsrc;

    SceUID fddst = sceIoOpen(dst_path, SCE_O_WRONLY | SCE_O_CREAT | SCE_O_TRUNC, 0777);
    if (fddst < 0)
    {
        sceIoClose(fdsrc);
        return fddst;
    }

    void *buf = memalign(4096, TRANSFER_SIZE);

    while (1)
    {
        int read = sceIoRead(fdsrc, buf, TRANSFER_SIZE);

        if (read < 0)
        {
            free(buf);

            sceIoClose(fddst);
            sceIoClose(fdsrc);

            sceIoRemove(dst_path);

            return read;
        }

        if (read == 0)
            break;

        int written = sceIoWrite(fddst, buf, read);

        if (written < 0)
        {
            free(buf);

            sceIoClose(fddst);
            sceIoClose(fdsrc);

            sceIoRemove(dst_path);

            return written;
        }
    }

    free(buf);

    // Inherit file stat
    SceIoStat stat;
    memset(&stat, 0, sizeof(SceIoStat));
    sceIoGetstatByFd(fdsrc, &stat);
    sceIoChstatByFd(fddst, &stat, 0x3B);

    sceIoClose(fddst);
    sceIoClose(fdsrc);

    return 1;
}

int CopyPath(const char *src_path, const char *dst_path)
{
    // The source and destination paths are identical
    if (strcasecmp(src_path, dst_path) == 0)
    {
        return -1;
    }

    // The destination is a subfolder of the source folder
    int len = strlen(src_path);
    if (strncasecmp(src_path, dst_path, len) == 0 && (dst_path[len] == '/' || dst_path[len - 1] == '/'))
    {
        return -1;
    }

    SceUID dfd = sceIoDopen(src_path);
    if (dfd >= 0)
    {
        SceIoStat stat;
        memset(&stat, 0, sizeof(SceIoStat));
        sceIoGetstatByFd(dfd, &stat);

        stat.st_mode |= SCE_S_IWUSR;

        int ret = sceIoMkdir(dst_path, stat.st_mode & 0xFFF);
        if (ret < 0 && ret != SCE_ERROR_ERRNO_EEXIST)
        {
            sceIoDclose(dfd);
            return ret;
        }

        if (ret == SCE_ERROR_ERRNO_EEXIST)
        {
            sceIoChstat(dst_path, &stat, 0x3B);
        }

        int res = 0;

        do
        {
            SceIoDirent dir;
            memset(&dir, 0, sizeof(SceIoDirent));

            res = sceIoDread(dfd, &dir);
            if (res > 0)
            {
                char *new_src_path = malloc(strlen(src_path) + strlen(dir.d_name) + 2);
                snprintf(new_src_path, MAX_PATH_LENGTH, "%s%s%s", src_path, HasEndSlash(src_path) ? "" : "/", dir.d_name);

                char *new_dst_path = malloc(strlen(dst_path) + strlen(dir.d_name) + 2);
                snprintf(new_dst_path, MAX_PATH_LENGTH, "%s%s%s", dst_path, HasEndSlash(dst_path) ? "" : "/", dir.d_name);

                if (SCE_S_ISDIR(dir.d_stat.st_mode))
                {
                    ret = CopyPath(new_src_path, new_dst_path);
                }
                else
                {
                    ret = CopyFile(new_src_path, new_dst_path);
                }

                free(new_dst_path);
                free(new_src_path);

                if (ret <= 0)
                {
                    sceIoDclose(dfd);
                    return ret;
                }
            }
        } while (res > 0);

        sceIoDclose(dfd);
    }
    else
    {
        return CopyFile(src_path, dst_path);
    }

    return 1;
}

int MovePath(const char *src_path, const char *dst_path)
{
    // The source and destination paths are identical
    if (strcasecmp(src_path, dst_path) == 0)
    {
        return -1;
    }

    // The destination is a subfolder of the source folder
    int len = strlen(src_path);
    if (strncasecmp(src_path, dst_path, len) == 0 && (dst_path[len] == '/' || dst_path[len - 1] == '/'))
    {
        return -1;
    }

    int ret = sceIoRename(src_path, dst_path);

    if (ret < 0)
    {
        if (ret != SCE_ERROR_ERRNO_EEXIST)
            return ret;

        int res;

        // Src stat
        SceIoStat src_stat;
        memset(&src_stat, 0, sizeof(SceIoStat));
        res = sceIoGetstat(src_path, &src_stat);
        if (res < 0)
            return res;

        // Dst stat
        SceIoStat dst_stat;
        memset(&dst_stat, 0, sizeof(SceIoStat));
        res = sceIoGetstat(dst_path, &dst_stat);
        if (res < 0)
            return res;

        // Is dir
        int src_is_dir = SCE_S_ISDIR(src_stat.st_mode);
        int dst_is_dir = SCE_S_ISDIR(dst_stat.st_mode);

        // One of them is a file and the other a directory, no replacement or integration possible
        if (src_is_dir != dst_is_dir)
            return ret;

        if (!src_is_dir && !dst_is_dir) // Replace file
        {
            sceIoRemove(dst_path);

            res = sceIoRename(src_path, dst_path);
            if (res < 0)
                return res;

            return 1;
        }
        else if (src_is_dir && dst_is_dir) // Integrate directory
        {
            SceUID dfd = sceIoDopen(src_path);
            if (dfd < 0)
                return dfd;

            do
            {
                SceIoDirent dir;
                memset(&dir, 0, sizeof(SceIoDirent));

                res = sceIoDread(dfd, &dir);
                if (res > 0)
                {
                    char *new_src_path = malloc(strlen(src_path) + strlen(dir.d_name) + 2);
                    snprintf(new_src_path, MAX_PATH_LENGTH, "%s%s%s", src_path, HasEndSlash(src_path) ? "" : "/", dir.d_name);

                    char *new_dst_path = malloc(strlen(dst_path) + strlen(dir.d_name) + 2);
                    snprintf(new_dst_path, MAX_PATH_LENGTH, "%s%s%s", dst_path, HasEndSlash(dst_path) ? "" : "/", dir.d_name);

                    // Recursive move
                    ret = MovePath(new_src_path, new_dst_path);

                    free(new_dst_path);
                    free(new_src_path);

                    if (ret <= 0)
                    {
                        sceIoDclose(dfd);
                        return ret;
                    }
                }
            } while (res > 0);

            sceIoDclose(dfd);

            // Integrated, now remove this directory
            sceIoRmdir(src_path);
        }
    }

    return 1;
}

int WritePngFile(const char *path, unsigned char *pixels, int width, int height, int bit_depth)
{
    FILE *png_file = fopen(path, "wb");
    if (png_file == NULL)
    {
        return -1;
    }

    png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    if (png_ptr == NULL)
    {
        fclose(png_file);
        return -1;
    }

    png_infop info_ptr = png_create_info_struct(png_ptr);
    if (info_ptr == NULL)
    {
        png_destroy_write_struct(&png_ptr, NULL);
        fclose(png_file);
        return -1;
    }

    png_bytepp row_ptr = (png_bytepp)png_malloc(png_ptr, height * sizeof(png_bytep));
    if (info_ptr == NULL)
    {
        png_destroy_write_struct(&png_ptr, &info_ptr);
        fclose(png_file);
        return -1;
    }

    int i;
    for (i = 0; i < height; i++)
    {
        row_ptr[i] = (png_bytep)(pixels + i * width * 4);
    }

    png_init_io(png_ptr, png_file);
    png_set_packing(png_ptr);
    png_set_IHDR(png_ptr, info_ptr, width, height, bit_depth, PNG_COLOR_TYPE_RGB_ALPHA,
                 PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
    png_write_info(png_ptr, info_ptr);
    png_write_image(png_ptr, row_ptr);
    png_write_end(png_ptr, info_ptr);
    png_free(png_ptr, row_ptr);
    png_destroy_write_struct(&png_ptr, &info_ptr);
    fclose(png_file);

    return 0;
}
